
#include <QDebug>
#include <QFileInfo>
#include <QFile>
#include <QRegularExpression>
#include <QSysInfo>

#include "dpkg.h"

class DpkgPrivate
{
public:
    DpkgPrivate(Dpkg *parent = nullptr);
    ~DpkgPrivate();

    QList<PKGInfo> installed;
    QList<PKGInfo> available;

    bool installed_loaded;
    bool available_loaded;

    QRegularExpression re;
    PKGInfo empty_info;

public:
    void LoadStatus(QList<PKGInfo> &pkgs, const QString &status_file);
    void LoadInstalled();
    void LoadAvailable();
    void EmitUpdated();
    const PKGInfo &GetPackageInfo(const QString &name);

private:
    Dpkg * const q_ptr;
    Q_DECLARE_PUBLIC(Dpkg)
};

DpkgPrivate::DpkgPrivate(Dpkg *parent) : q_ptr(parent)
{
    installed_loaded = false;
    available_loaded = false;

    re.setPattern(QLatin1String("[\n\r]^[\n\r]+"));
    re.setPatternOptions(QRegularExpression::MultilineOption);
}

DpkgPrivate::~DpkgPrivate()
{}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
void StringSplit(QStringList &list, const QString &str, const QString &split, Qt::SplitBehavior behavior = Qt::KeepEmptyParts)
#else
void StringSplit(QStringList &list, const QString &str, const QString &split, QString::SplitBehavior behavior = QString::KeepEmptyParts)
#endif
{
    list.clear();
    list = str.split(split, behavior);
    for (int i=0; i<list.count(); i++) {
        list[i] = list.at(i).trimmed();
    }
}

void FieldDepends(QList<PKGDepend> &dep, const QString &key, const QString &value, bool allow_alternatives = true)
{
    QStringList v_items; StringSplit(v_items, value, QLatin1String(","));
    for (const QString &v_item : qAsConst(v_items)) {
        if (v_item.contains(QLatin1String("|"))) {
            if (!allow_alternatives) {
                qCritical().noquote() << QString(QLatin1String("alternatives ('|') not allowed in '%1' field")).arg(key);
                return;
            } else {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
                Qt::SplitBehavior behavior = Qt::SkipEmptyParts;
#else
                QString::SplitBehavior behavior = QString::SkipEmptyParts;
#endif
                QStringList a_items; StringSplit(a_items, v_item, QLatin1String("|"), behavior);
                if (a_items.count() > 1) {
                    PKGDepend depend;
                    for (const QString &a_item : qAsConst(a_items)) {
                        if (depend.package->name.isEmpty()) {
                            depend.fromString(a_item, key);
                        } else {
                            depend.alternatives.append(QSharedPointer<PKGDepend>(new PKGDepend(a_item, key)));
                        }
                    }
                    dep.append(depend);
                } else if (a_items.count() == 1) {
                    dep.append(PKGDepend(a_items.at(0), key));
                }
            }
        } else {
            dep.append(PKGDepend(v_item, key));
        }
    }
}

void FieldArchive(QList<PKGArchiveDetails>& archives, const QString &key, const QString &value)
{
    QStringList items = value.split(QLatin1String(" "));
    if (!archives.isEmpty()) {
        if (items.count() != archives.count()) {
            qCritical().noquote() << "'Filenames' count not match other parts";
            return;
        } else {
            for (int i=0; i<items.count(); i++) {
                PKGArchiveDetails &archive = archives[i];
                archive.field(key, items.at(i));
            }
        }
    } else {
        for (const QString &item : qAsConst((items))) {
            archives.append(PKGArchiveDetails(item));
        }
    }
};

void UpdateRevision(PKGVersion &version, const QString &key, const QString &value)
{
    qWarning().noquote() << QString(QLatin1String("obsolete '%1' field used")).arg(key);
    version.version = QString(QLatin1String("%1-%2")).arg(version.version, version.revision);
    version.revision = value;
}

// https://wiki.debian.org/DebianRepository/Format
void PKGParseLine(PKGInfo &info, const QString &line)
{
    QStringList kv = line.split(QLatin1String(": "));
    const QString &key = kv.at(0);
    const QString &value = kv.at(1);
    if (key == QLatin1String("Package")) { info.name = value; }
    if (key == QLatin1String("Essential")) { info.essential = (value == QLatin1String("yes") ? true : false); }
    if (key == QLatin1String("Protected")) { info.is_protected = (value == QLatin1String("yes") ? true : false); }
    if (key == QLatin1String("Status")) {
        QStringList st_items = value.split(QLatin1String(" "));
        info.want = info.ConvWant(st_items.at(0));
        info.eflag = info.ConvEflag(st_items.at(1));
        info.status = info.ConvStatus(st_items.at(2));
    }
    if (key == QLatin1String("Priority")) { info.priority = info.ConvPriority(value); }
    if (key == QLatin1String("Section")) { info.section = value; }
    if (key == QLatin1String("Installed-Size")) { info.installedsize = value; }
    if (key == QLatin1String("Origin")) { info.origin = value; }
    if (key == QLatin1String("Maintainer")) { info.maintainer = value; }
    if (key == QLatin1String("Bugs")) { info.bugs = value; }
    if (key == QLatin1String("Architecture")) { info.arch = PKGArch(value); }
    if (key == QLatin1String("Multi-Arch")) { info.multiarch = info.ConvMultiArch(value); }
    if (key == QLatin1String("Source")) { info.source = value; }
    if (key == QLatin1String("Version")) { info.version = PKGVersion(value); }
    if (key == QLatin1String("Config-Version")) { info.configversion = PKGVersion(value); }
    if (key == QLatin1String("Replaces")) { FieldDepends(info.dep_replaces, key, value, false); }
    if (key == QLatin1String("Provides")) { FieldDepends(info.dep_provides, key, value, false); }
    if (key == QLatin1String("Depends")) { FieldDepends(info.dep_depends, key, value); }
    if (key == QLatin1String("Pre-Depends")) { FieldDepends(info.dep_predepends, key, value); }
    if (key == QLatin1String("Recommends")) { FieldDepends(info.dep_recommends, key, value); }
    if (key == QLatin1String("Suggests")) { FieldDepends(info.dep_suggests, key, value); }
    if (key == QLatin1String("Breaks")) { FieldDepends(info.dep_breaks, key, value, false); }
    if (key == QLatin1String("Conflicts")) { FieldDepends(info.dep_conflicts, key, value, false); }
    if (key == QLatin1String("Enhances")) { FieldDepends(info.dep_enhances, key, value); }
    if (key == QLatin1String("Conffiles")) { /* Process at last */ }
    if (key == QLatin1String("Filename")) { FieldArchive(info.archives, key, value); }
    if (key == QLatin1String("Size")) { FieldArchive(info.archives, key, value); }
    if (key == QLatin1String("MD5sum")) { FieldArchive(info.archives, key, value); }
    if (key == QLatin1String("MSDOS-Filename")) { FieldArchive(info.archives, key, value); }
    if (key == QLatin1String("Description")) { info.summary = value; }
    if (key == QLatin1String("Triggers-Pending")) {  }
    if (key == QLatin1String("Triggers-Awaited")) {  }
    if (key == QLatin1String("Revision")) { UpdateRevision(info.version, key, value); }
    if (key == QLatin1String("Recommended")) { FieldDepends(info.dep_recommended, key, value); }
    if (key == QLatin1String("Optional")) { FieldDepends(info.dep_optional, key, value); }
    if (key == QLatin1String("Class")) { info.package_class = info.ConvPriority(value); }
    if (key == QLatin1String("Package-Revision")) { UpdateRevision(info.version, key, value); }
    if (key == QLatin1String("Package_Revision")) { UpdateRevision(info.version, key, value); }
}

void DpkgPrivate::LoadStatus(QList<PKGInfo> &pkgs, const QString &status_file)
{
    QFileInfo info(status_file);
    if (!info.isFile()) {
        qCritical().noquote() << "Status file is not a file.";
        return;
    }
    if (!info.isReadable()) {
        qCritical().noquote() << "Status file not readable.";
        return;
    }
    QFile file(info.absoluteFilePath());
    file.open(QIODevice::ReadOnly);
    QString contents = QLatin1String(file.readAll());
    file.close();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
    Qt::SplitBehavior behavior = Qt::SkipEmptyParts;
#else
    QString::SplitBehavior behavior = QString::SkipEmptyParts;
#endif
    QStringList packages = contents.split(re, behavior);

    const QLatin1String remove_on_upgrade("remove-on-upgrade");
    const QLatin1String obsolete("obsolete");

    for (const QString &package : qAsConst(packages)) {
        PKGInfo pkg;
        QStringList lines = package.split(QLatin1String("\n"));
        QStringList desc_lines;
        QStringList conf_lines;
        bool desc_block = false;
        bool conf_block = false;
        for (const QString &line : qAsConst(lines)) {
            if (line.startsWith(QLatin1String("Description:"))) {
                desc_block = true;
                PKGParseLine(pkg, line);
                continue;
            }
            if (line.startsWith(QLatin1String("Conffiles:"))) {
                conf_block = true;
                continue;
            }
            if (line.startsWith(QLatin1String(" "))) {
                if (desc_block) {
                    desc_lines.append(line.right(line.count() - 1));
                    continue;
                }
                if (conf_block) {
                    conf_lines.append(line.right(line.count() - 1));
                    continue;
                }
            } else {
                desc_block = false;
                conf_block = false;
                PKGParseLine(pkg, line);
            }
        }
        if (!desc_lines.isEmpty()) {
            pkg.description = desc_lines.join(QLatin1String("\n"));
        }
        if (!conf_lines.isEmpty()) {
            for (const QString &conf_line : qAsConst(conf_lines)) {
                QStringList items = conf_line.split(QLatin1String(" "));
                PKGConfFile conffile(items.at(0), items.at(1), false, false);
                if (conffile.hash.length() == (QString(remove_on_upgrade).length() - 1)) {
                    conffile.hash = remove_on_upgrade;
                    conffile.remove_on_upgrade = true;
                }
                if (conffile.hash.length() == (QString(obsolete).length() - 1)) {
                    conffile.hash = obsolete;
                    conffile.obsolete = true;
                }
                pkg.conffiles.append(conffile);
            }
        }
        pkgs.append(pkg);
    }
}

void DpkgPrivate::LoadInstalled()
{
    installed.clear();
    LoadStatus(installed, QLatin1String("/var/lib/dpkg/status"));
    installed_loaded = true;
    EmitUpdated();
}

void DpkgPrivate::LoadAvailable()
{
    available.clear();
    LoadStatus(available, QLatin1String("/var/lib/dpkg/available"));
    available_loaded = true;
    EmitUpdated();
}

void DpkgPrivate::EmitUpdated()
{
    Q_Q(Dpkg);
    if (installed_loaded && available_loaded) {
        emit q->Updated();
    }
}

const PKGInfo &DpkgPrivate::GetPackageInfo(const QString &name)
{
    for (const PKGInfo &info : qAsConst(installed)) {
        if (info.name == name) {
            return info;
        }
    }
    for (const PKGInfo &info : qAsConst(available)) {
        if (info.name == name) {
            return info;
        }
    }
    return this->empty_info;
}

Dpkg::Dpkg(QObject *parent) : QObject{parent}, d_ptr(new DpkgPrivate(this))
{
}

Dpkg::~Dpkg()
{}

void Dpkg::LoadStatus()
{
    Q_D(Dpkg);
    d->LoadInstalled();
    d->LoadAvailable();
}

const QList<PKGInfo> &Dpkg::available()
{
    Q_D(Dpkg);
    return d->available;
}

const QList<PKGInfo> &Dpkg::installed()
{
    Q_D(Dpkg);
    return d->installed;
}

const PKGInfo &Dpkg::GetPackageInfo(const QString &name)
{
    Q_D(Dpkg);
    return d->GetPackageInfo(name);
}
